/* * jwerty - Awesome handling of keyboard events * * jwerty is a JS lib which allows you to bind, fire and assert key combination * strings against elements and events. It normalises the poor std api into * something easy to use and clear. * * This code is licensed under the MIT * For the full license see: http://keithamus.mit-license.org/ * For more information see: http://keithamus.github.com/jwerty * * @author Keith Cirkel ('keithamus') * @license http://keithamus.mit-license.org/ * @copyright Copyright © 2011, Keith Cirkel * */ (function (global, exports) { // Try require external librairies in Node.js context function tryRequire(mod) { if (typeof require == 'function' && typeof module !== 'undefined' && module.exports) { try { return require(mod.toLowerCase()); } catch (err) {} } else { return global[mod]; } } // Helper methods & vars: var $d = global.document, $ = (tryRequire('jquery') || tryRequire('zepto') || tryRequire('ender') || $d), $$, // Element selector function $b, // Event binding function $u, // Event unbinding function $f, // Event firing function ke = 'keydown'; function realTypeOf(v, s) { return (v === null) ? s === 'null' : (v === undefined) ? s === 'undefined' : (v.is && v instanceof $) ? s === 'element' : Object.prototype.toString.call(v).toLowerCase().indexOf(s) > 7; } if ($ === $d) { $$ = function (selector, context) { return selector ? $.querySelector(selector, context || $) : $; }; $b = function (e, fn) { e.addEventListener(ke, fn, false); }; $u = function (e, fn) { e.removeEventListener(ke, fn, false); }; $f = function (e, jwertyEv) { var ret = $d.createEvent('Event'), i; ret.initEvent(ke, true, true); for (i in jwertyEv) ret[i] = jwertyEv[i]; return (e || $).dispatchEvent(ret); }; } else { $$ = function (selector, context) { return $(selector || $d, context); }; $b = function (e, fn) { $(e).bind(ke + '.jwerty', fn); }; $u = function (e, fn) { $(e).unbind(ke + '.jwerty', fn) }; $f = function (e, ob) { $(e || $d).trigger($.Event(ke, ob)); }; } // Private var _modProps = { 16: 'shiftKey', 17: 'ctrlKey', 18: 'altKey', 91: 'metaKey' }; // Generate key mappings for common keys that are not printable. var _keys = { // MOD aka toggleable keys mods: { // Shift key, ⇧ '⇧': 16, shift: 16, // CTRL key, on Mac: ⌃ '⌃': 17, ctrl: 17, // ALT key, on Mac: ⌥ (Alt) '⌥': 18, alt: 18, option: 18, // META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super) '⌘': 91, meta: 91, cmd: 91, 'super': 91, win: 91 }, // Normal keys keys: { // Backspace key, on Mac: ⌫ (Backspace) '⌫': 8, backspace: 8, // Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥ '⇥': 9, '⇆': 9, tab: 9, // Return key, ↩ '↩': 13, 'return': 13, enter: 13, '⌅': 13, // Pause/Break key 'pause': 19, 'pause-break': 19, // Caps Lock key, ⇪ '⇪': 20, caps: 20, 'caps-lock': 20, // Escape key, on Mac: ⎋, on Windows: Esc '⎋': 27, escape: 27, esc: 27, // Space key space: 32, // Page-Up key, or pgup, on Mac: ↖ '↖': 33, pgup: 33, 'page-up': 33, // Page-Down key, or pgdown, on Mac: ↘ '↘': 34, pgdown: 34, 'page-down': 34, // END key, on Mac: ⇟ '⇟': 35, end: 35, // HOME key, on Mac: ⇞ '⇞': 36, home: 36, // Insert key, or ins ins: 45, insert: 45, // Delete key, on Mac: ⌫ (Delete) del: 46, 'delete': 46, // Left Arrow Key, or ← '←': 37, left: 37, 'arrow-left': 37, // Up Arrow Key, or ↑ '↑': 38, up: 38, 'arrow-up': 38, // Right Arrow Key, or → '→': 39, right: 39, 'arrow-right': 39, // Up Arrow Key, or ↓ '↓': 40, down: 40, 'arrow-down': 40, // odities, printing characters that come out wrong: // Num-Multiply, or * '*': 106, star: 106, asterisk: 106, multiply: 106, // Num-Plus or + '+': 107, 'plus': 107, // Num-Subtract, or - '-': 109, subtract: 109, 'num-.': 110, 'num-period': 110, 'num-dot': 110, 'num-full-stop': 110, 'num-delete': 110, // Semicolon ';': 186, semicolon: 186, // = or equals '=': 187, 'equals': 187, // Comma, or , ',': 188, comma: 188, //'-': 189, //??? // Period, or ., or full-stop '.': 190, period: 190, 'full-stop': 190, // Slash, or /, or forward-slash '/': 191, slash: 191, 'forward-slash': 191, // Tick, or `, or back-quote '`': 192, tick: 192, 'back-quote': 192, // Open bracket, or [ '[': 219, 'open-bracket': 219, // Back slash, or \ '\\': 220, 'back-slash': 220, // Close backet, or ] ']': 221, 'close-bracket': 221, // Apostraphe, or Quote, or ' '\'': 222, quote: 222, apostraphe: 222 } }; // To minimise code bloat, add all of the 0-9 and NUMPAD 0-9 keys in a loop var i = 47, n = 0; while (++i < 106) { _keys.keys[n] = i; _keys.keys['num-' + n] = i + 48; ++n; } // To minimise code bloat, add all of the F1-F25 keys in a loop i = 111, n = 1; while (++i < 136) { _keys.keys['f' + n] = i; ++n; } // To minimise code bloat, add all of the letters of the alphabet in a loop i = 64; while (++i < 91) { _keys.keys[String.fromCharCode(i).toLowerCase()] = i; } function JwertyCode(jwertyCode) { var i, c, n, z, keyCombo, optionals, jwertyCodeFragment, rangeMatches, rangeI; // In-case we get called with an instance of ourselves, just return that. if (jwertyCode instanceof JwertyCode) return jwertyCode; // If jwertyCode isn't an array, cast it as a string and split into array. if (!realTypeOf(jwertyCode, 'array')) { jwertyCode = (String(jwertyCode)).replace(/\s/g, '').toLowerCase() .match(/(?:\+,|[^,])+/g); } // Loop through each key sequence in jwertyCode for (i = 0, c = jwertyCode.length; i < c; ++i) { // If the key combo at this part of the sequence isn't an array, // cast as a string and split into an array. if (!realTypeOf(jwertyCode[i], 'array')) { jwertyCode[i] = String(jwertyCode[i]) .match(/(?:\+\/|[^\/])+/g); } // Parse the key optionals in this sequence optionals = [], n = jwertyCode[i].length; while (n--) { // Begin creating the object for this key combo jwertyCodeFragment = jwertyCode[i][n]; keyCombo = { jwertyCombo: String(jwertyCodeFragment), shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }; // If jwertyCodeFragment isn't an array then cast as a string // and split it into one. if (!realTypeOf(jwertyCodeFragment, 'array')) { jwertyCodeFragment = String(jwertyCodeFragment).toLowerCase() .match(/(?:(?:[^\+])+|\+\+|^\+$)/g); } z = jwertyCodeFragment.length; while (z--) { // Normalise matching errors if (jwertyCodeFragment[z] === '++') jwertyCodeFragment[z] = '+'; // Inject either keyCode or ctrl/meta/shift/altKey into keyCombo if (jwertyCodeFragment[z] in _keys.mods) { keyCombo[_modProps[_keys.mods[jwertyCodeFragment[z]]]] = true; } else if (jwertyCodeFragment[z] in _keys.keys) { keyCombo.keyCode = _keys.keys[jwertyCodeFragment[z]]; } else { rangeMatches = jwertyCodeFragment[z].match(/^\[([^-]+\-?[^-]*)-([^-]+\-?[^-]*)\]$/); } } if (realTypeOf(keyCombo.keyCode, 'undefined')) { // If we picked up a range match earlier... if (rangeMatches && (rangeMatches[1] in _keys.keys) && (rangeMatches[2] in _keys.keys)) { rangeMatches[2] = _keys.keys[rangeMatches[2]]; rangeMatches[1] = _keys.keys[rangeMatches[1]]; // Go from match 1 and capture all key-comobs up to match 2 for (rangeI = rangeMatches[1]; rangeI < rangeMatches[2]; ++rangeI) { optionals.push({ altKey: keyCombo.altKey, shiftKey: keyCombo.shiftKey, metaKey: keyCombo.metaKey, ctrlKey: keyCombo.ctrlKey, keyCode: rangeI, jwertyCombo: String(jwertyCodeFragment) }); } keyCombo.keyCode = rangeI; // Inject either keyCode or ctrl/meta/shift/altKey into keyCombo } else { keyCombo.keyCode = 0; } } optionals.push(keyCombo); } this[i] = optionals; } this.length = i; return this; } var jwerty = exports.jwerty = { /** * jwerty.event * * `jwerty.event` will return a function, which expects the first * argument to be a key event. When the key event matches `jwertyCode`, * `callbackFunction` is fired. `jwerty.event` is used by `jwerty.key` * to bind the function it returns. `jwerty.event` is useful for * attaching to your own event listeners. It can be used as a decorator * method to encapsulate functionality that you only want to fire after * a specific key combo. If `callbackContext` is specified then it will * be supplied as `callbackFunction`'s context - in other words, the * keyword `this` will be set to `callbackContext` inside the * `callbackFunction` function. * * @param {Mixed} jwertyCode can be an array, or string of key * combinations, which includes optinals and or sequences * @param {Function} callbackFucntion is a function (or boolean) which * is fired when jwertyCode is matched. Return false to * preventDefault() * @param {Object} callbackContext (Optional) The context to call * `callback` with (i.e this) * */ event: function (jwertyCode, callbackFunction, callbackContext /*? this */) { // Construct a function out of callbackFunction, if it is a boolean. if (realTypeOf(callbackFunction, 'boolean')) { var bool = callbackFunction; callbackFunction = function () { return bool; }; } jwertyCode = new JwertyCode(jwertyCode); // Initialise in-scope vars. var i = 0, c = jwertyCode.length - 1, returnValue, jwertyCodeIs; // This is the event listener function that gets returned... return function (event) { // if jwertyCodeIs returns truthy (string)... if ((jwertyCodeIs = jwerty.is(jwertyCode, event, i))) { // ... and this isn't the last key in the sequence, // incriment the key in sequence to check. if (i < c) { ++i; return; // ... and this is the last in the sequence (or the only // one in sequence), then fire the callback } else { returnValue = callbackFunction.call( callbackContext || this, event, jwertyCodeIs); // If the callback returned false, then we should run // preventDefault(); if (returnValue === false) event.preventDefault(); // Reset i for the next sequence to fire. i = 0; return; } } // If the event didn't hit this time, we should reset i to 0, // that is, unless this combo was the first in the sequence, // in which case we should reset i to 1. i = jwerty.is(jwertyCode, event) ? 1 : 0; }; }, /** * jwerty.is * * `jwerty.is` will return a boolean value, based on if `event` matches * `jwertyCode`. `jwerty.is` is called by `jwerty.event` to check * whether or not to fire the callback. `event` can be a DOM event, or * a jQuery/Zepto/Ender manufactured event. The properties of * `jwertyCode` (speficially ctrlKey, altKey, metaKey, shiftKey and * keyCode) should match `jwertyCode`'s properties - if they do, then * `jwerty.is` will return `true`. If they don't, `jwerty.is` will * return `false`. * * @param {Mixed} jwertyCode can be an array, or string of key * combinations, which includes optinals and or sequences * @param {KeyboardEvent} event is the KeyboardEvent to assert against * @param {Integer} i (Optional) checks the `i` key in jwertyCode * sequence * */ is: function (jwertyCode, event, i /*? 0*/) { jwertyCode = new JwertyCode(jwertyCode); // Default `i` to 0 i = i || 0; // We are only interested in `i` of jwertyCode; jwertyCode = jwertyCode[i]; // jQuery stores the *real* event in `originalEvent`, which we use // because it does annoything stuff to `metaKey` event = event.originalEvent || event; // We'll look at each optional in this jwertyCode sequence... var n = jwertyCode.length, returnValue = false; // Loop through each fragment of jwertyCode while (n--) { returnValue = jwertyCode[n].jwertyCombo; // For each property in the jwertyCode object, compare to `event` for (var p in jwertyCode[n]) { // ...except for jwertyCode.jwertyCombo... if (p !== 'jwertyCombo' && event[p] != jwertyCode[n][p]) returnValue = false; } // If this jwertyCode optional wasn't falsey, then we can return early. if (returnValue !== false) return returnValue; } return returnValue; }, /** * jwerty.key * * `jwerty.key` will attach an event listener and fire * `callbackFunction` when `jwertyCode` matches. The event listener is * attached to `document`, meaning it will listen for any key events * on the page (a global shortcut listener). If `callbackContext` is * specified then it will be supplied as `callbackFunction`'s context * - in other words, the keyword `this` will be set to * `callbackContext` inside the `callbackFunction` function. * returns a subscription handle `h`, by which you may undo the binding * by calling `h.unbind()` * * @param {Mixed} jwertyCode can be an array, or string of key * combinations, which includes optinals and or sequences * @param {Function} callbackFunction is a function (or boolean) which * is fired when jwertyCode is matched. Return false to * preventDefault() * @param {Object} callbackContext (Optional) The context to call * `callback` with (i.e this) * @param {Mixed} selector can be a string, jQuery/Zepto/Ender object, * or an HTML*Element on which to bind the eventListener * @param {Mixed} selectorContext can be a string, jQuery/Zepto/Ender * object, or an HTML*Element on which to scope the selector * */ key: function (jwertyCode, callbackFunction, callbackContext /*? this */, selector /*? document */, selectorContext /*? body */) { // Because callbackContext is optional, we should check if the // `callbackContext` is a string or element, and if it is, then the // function was called without a context, and `callbackContext` is // actually `selector` var realSelector = realTypeOf(callbackContext, 'element') || realTypeOf(callbackContext, 'string') ? callbackContext : selector, // If `callbackContext` is undefined, or if we skipped it (and // therefore it is `realSelector`), set context to `global`. realcallbackContext = realSelector === callbackContext ? global : callbackContext, // Finally if we did skip `callbackContext`, then shift // `selectorContext` to the left (take it from `selector`) realSelectorContext = realSelector === callbackContext ? selector : selectorContext; // If `realSelector` is already a jQuery/Zepto/Ender/DOM element, // then just use it neat, otherwise find it in DOM using $$() var element = realTypeOf(realSelector, 'element') ? realSelector : $$(realSelector, realSelectorContext); var callback = jwerty.event(jwertyCode, callbackFunction, realcallbackContext); $b( element, callback ); return {unbind:function(){ $u( element, callback ) }}; }, /** * jwerty.fire * * `jwerty.fire` will construct a keyup event to fire, based on * `jwertyCode`. The event will be fired against `selector`. * `selectorContext` is used to search for `selector` within * `selectorContext`, similar to jQuery's * `$('selector', 'context')`. * * @param {Mixed} jwertyCode can be an array, or string of key * combinations, which includes optinals and or sequences * @param {Mixed} selector can be a string, jQuery/Zepto/Ender object, * or an HTML*Element on which to bind the eventListener * @param {Mixed} selectorContext can be a string, jQuery/Zepto/Ender * object, or an HTML*Element on which to scope the selector * */ fire: function (jwertyCode, selector /*? document */, selectorContext /*? body */, i) { jwertyCode = new JwertyCode(jwertyCode); var realI = realTypeOf(selectorContext, 'number') ? selectorContext : i; // If `realSelector` is already a jQuery/Zepto/Ender/DOM element, // then just use it neat, otherwise find it in DOM using $$() $f( realTypeOf(selector, 'element') ? selector : $$(selector, selectorContext), jwertyCode[realI || 0][0] ); }, KEYS: _keys }; }(typeof global !== 'undefined' && global.window || this, (typeof module !== 'undefined' && module.exports ? module.exports : this)));